﻿/*
 * fbasync - An Asynchronous Facebook API for .NET
 * Copyright (C) 2007 ittybittysoft
 * Email: info@ittybittysoft.com
 * Portions of the library are based on the Facebook Developer Toolkit
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web;
using System.Web.Security;

namespace fbasync.webcomponents
{
    public static class FacebookConnectionExtensions
    {
        internal const string UserConnectionContextKey = "fbasync.userconnection";

        public static IAsyncResult BeginGetUser(this FacebookConnection conn, long uid, AsyncCallback cb, object state)
        {
            return conn.BeginGetUsers(String.Format("uid = {0}", uid), cb, state);
        }

        public static User EndGetUser(this FacebookConnection conn, IAsyncResult ar)
        {
            return conn.EndGetUsers(ar).FirstOrDefault();
        }

        /// <exception cref="InvalidOperationException">FacebookConnection.UserId should be specified first.</exception>
        public static IAsyncResult BeginGetUser(this FacebookConnection conn, AsyncCallback cb, object state)
        {
            if (conn.UserId == null) throw new InvalidOperationException("FacebookConnection.UserId should be specified first.");
            return conn.BeginGetUser(conn.UserId.Value, cb, state);
        }

        public static bool HasSignature(this HttpContext context, string applicationKey)
        {
            return null != context.Request.Params["fb_sig"] || null != context.Request.Params[applicationKey];
        }

        public static bool IsConnectSignatureValid(this HttpContext context, string applicationKey, string secret)
        {
            return IsSignatureValid(context, applicationKey + "_", applicationKey, secret);
        }

        public static bool IsFrameSignatureValid(this HttpContext context, string secret)
        {
            return IsSignatureValid(context, "fb_sig_", "fb_sig", secret);
        }

        private static bool IsSignatureValid(this HttpContext context, string sigPrefix, string sigParam, string secret)
        {
            HttpRequest request = context.Request;
            Dictionary<string, string> parameters = new Dictionary<string, string>();

            var filteredParams = request.Params.AllKeys
                .Where(s => s != null)
                .Where(s => s.StartsWith(sigPrefix));

            int sigPrefixLen = sigPrefix.Length;
            foreach (string key in filteredParams)
                parameters.Add(key.Substring(sigPrefixLen), request.Params[key]);

            return request.Params[sigParam] == FacebookConnection.GenerateSignature(parameters, secret);
        }

        public static bool IsSignatureValid(this HttpContext context, string applicationKey, string secret)
        {
            return IsFrameSignatureValid(context, secret) || IsConnectSignatureValid(context, applicationKey, secret);
        }

        public static FacebookConnection GetAmbientSession(this HttpContext ctx, FacebookConnection connectionTemplate)
        {
            if (null == connectionTemplate) return null;

            if (!ctx.HasSignature(connectionTemplate.ApplicationKey)) return connectionTemplate.Clone(true, true);

            if (ctx.IsFrameSignatureValid(connectionTemplate.Secret))
            {
                //prefer fb_sig params
                var cloned = connectionTemplate.Clone(true, true);
                cloned.SessionKey = ctx.Request.Params["fb_sig_session_key"];

                long uid;
                bool hasUid = long.TryParse(ctx.Request.Params["fb_sig_user"], out uid);
                if (!hasUid)
                    hasUid = long.TryParse(ctx.Request.Params["fb_sig_canvas_user"], out uid);

                if (hasUid)
                    cloned.UserId = uid;

                //there is no authenticated ambient session
                if (null == cloned.SessionKey) return cloned;

                cloned.SessionExpiresUnixTime = double.Parse(ctx.Request.Params["fb_sig_expires"]);
                return cloned;
            }
            else if (ctx.IsConnectSignatureValid(connectionTemplate.ApplicationKey, connectionTemplate.Secret))
            {
                //fallback to connect params if no fb_sig
                var cloned = connectionTemplate.Clone(true, true);
                cloned.SessionKey = ctx.Request.Params[connectionTemplate.ApplicationKey + "_session_key"];

                //there is no authenticated ambient session
                if (null == cloned.SessionKey) return cloned;
                
                cloned.SessionExpiresUnixTime = double.Parse(ctx.Request.Params[connectionTemplate.ApplicationKey + "_expires"]);
                cloned.UserId = long.Parse(ctx.Request.Params[connectionTemplate.ApplicationKey + "_user"]);
                return cloned;
            }
            else
            {
                return connectionTemplate.Clone(true, true);
            }
        }

        public static FacebookConnection GetUserConnection(this HttpContext context)
        {
            return (FacebookConnection)context.Items[UserConnectionContextKey];
        }

        internal static void SetUserConnection(this HttpContext context, FacebookConnection cnn)
        {
            context.Items[UserConnectionContextKey] = cnn;
        }

        internal static FormsAuthenticationTicket ToAuthTicket(this FacebookConnection cnn, FormsAuthenticationTicket oldTicket, int authTimeout)
        {
            int newVersion = null == oldTicket ? 1 : oldTicket.Version + 1;
            DateTime now = DateTime.Now;

            FormsAuthenticationTicket t = new FormsAuthenticationTicket(
                newVersion,
                cnn.UserId.Value.ToString(),
                now,
                now.AddMinutes(authTimeout),
                true,
                cnn.Serialize(false, false));

            return t;
        }

        internal static FacebookConnection DeserializeConnection(this FormsAuthenticationTicket ticket, string secret)
        {
            FacebookConnection cnn = FacebookConnection.Deserialize(ticket.UserData);
            cnn.Secret = secret;
            return cnn;
        }
    }
}
